# Memory 与 Hooks 系统

> **一句话摘要**：6 层 CLAUDE.md Memory 系统（Managed→User→Project→Local→AutoMem→TeamMem）+ 约 28 种 Hook 事件，构成了 Claude Code 的持久化知识和自动化扩展基础。

> 核心文件：`src/utils/claudemd.ts`、`src/utils/hooks.ts`、`src/memdir/memdir.ts`、`src/memdir/memoryTypes.ts`、`src/types/hooks.ts`
>
> 相关笔记：[[13 - Prompt 体系总览]]、[[14 - System Prompt 架构]]、[[16 - Prompt Caching 与 Context 管理]]、[[05 - 权限系统]]、[[19 - Prompt 工程技巧与设计模式]]

## 一、CLAUDE.md Memory 系统

### 1.1 六层 Memory 类型

| 类型 | 路径 | 说明 |
|------|------|------|
| **Managed** | `/etc/claude-code/CLAUDE.md` | 全局企业策略（管理员设置） |
| **User** | `~/.claude/CLAUDE.md` | 用户私人全局指令 |
| **Project** | `CLAUDE.md`, `.claude/CLAUDE.md`, `.claude/rules/*.md` | 项目指令（检入代码库） |
| **Local** | `CLAUDE.local.md` | 私人项目指令（不检入） |
| **AutoMem** | `~/.claude/projects/<slug>/memory/MEMORY.md` | 自动记忆（跨会话持久化） |
| **TeamMem** | 团队记忆路径 | 团队共享记忆 |

### 1.2 加载优先级

```
后加载 = 更高优先级：
Managed → User → Project（从 root 向 CWD 遍历）→ Local → AutoMem → TeamMem
```

### 1.3 @include 指令

Memory 文件支持引用其他文件：
- `@path` — 相对路径（等同 `@./path`）
- `@./relative/path` — 相对路径
- `@~/home/path` — 主目录
- `@/absolute/path` — 绝对路径
- 最大递归深度：**5 层**（`MAX_INCLUDE_DEPTH = 5`）
- 循环引用通过 `processedPaths` Set 防止
- 使用 marked Lexer 解析 Markdown AST，**跳过 code block 和 codespan 中的 `@`**
- 支持转义空格：`@path\ with\ spaces`
- 支持 fragment 标识符剥离：`@file.md#section` → 只取 `file.md`
- **仅加载文本文件**：通过 `TEXT_FILE_EXTENSIONS` 白名单（80+ 种扩展名）过滤，防止二进制文件被加载进 memory

### 1.4 Frontmatter 条件规则

`.claude/rules/*.md` 支持条件加载：

```yaml
---
paths:
  - src/**/*.ts
  - lib/**/*.js
---
只有操作的文件匹配这些 glob 时才加载此规则
```

### 1.5 HTML 注释剥离

`<!-- -->` 块级注释在注入前被自动剥离（行内注释保留）。

## 二、Memory 如何注入 Prompt

### 注入路径

```
getSystemPrompt() ─┬─ systemPromptSection('memory') → loadMemoryPrompt()
                    │                                   → 自动记忆行为指令
                    │
getUserContext()  ──┴─ getClaudeMds() → 所有 CLAUDE.md 内容
                       → 作为第一条 user message 注入
```

### CLAUDE.md 内容格式

`MEMORY_INSTRUCTION_PROMPT` 完整文本：

```
Codebase and user instructions are shown below. Be sure to adhere to these instructions.
IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
```

每个 memory file 会附带类型说明后缀：

| 类型 | 后缀 |
|------|------|
| Project | `(project instructions, checked into the codebase)` |
| Local | `(user's private project instructions, not checked in)` |
| User | `(user's private global instructions for all projects)` |
| AutoMem | `(user's auto-memory, persists across conversations)` |
| TeamMem | `(shared team memory, synced across the organization)` |

最终注入格式：
```xml
<system-reminder>
Codebase and user instructions are shown below. Be sure to adhere to these instructions.
IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.

Contents of /path/CLAUDE.md (project instructions, checked into the codebase):
[实际 CLAUDE.md 内容]

Contents of ~/.claude/CLAUDE.md (user's private global instructions for all projects):
[用户全局指令]
</system-reminder>
```

> [!tip] TeamMem 特殊包装
> TeamMem 类型的内容会额外用 `<team-memory-content source="shared">` 标签包裹。

### AutoMem 记忆系统指令

`loadMemoryPrompt()` 生成约 60+ 行的行为指令，包括：

#### 四种记忆类型分类（`memoryTypes.ts`）

| 类型 | 说明 | 何时保存 |
|------|------|---------|
| **user** | 用户角色、目标、偏好 | 了解到用户身份、技能、职责时 |
| **feedback** | 用户对工作方式的正面/负面反馈 | 用户纠正做法 *或* 确认非显而易见的好做法时 |
| **project** | 项目进行中的工作、目标、事件 | 了解到谁在做什么、为什么、何时完成时 |
| **reference** | 外部系统的指针 | 了解到外部资源（Linear、Grafana、Slack 频道等）时 |

> [!important] 关键排除规则
> 以下内容 **不应保存为记忆**（即使用户要求）：
> - 代码模式、架构、文件路径 — 可从项目当前状态推导
> - Git 历史 — `git log`/`git blame` 才是权威来源
> - 调试方案 — 修复已在代码中，commit message 有上下文
> - CLAUDE.md 中已有的内容
> - 临时任务细节

#### 每条记忆的 frontmatter 格式

```markdown
---
name: {{memory name}}
description: {{one-line description — 用于未来对话判断相关性}}
type: {{user, feedback, project, reference}}
---

{{记忆内容 — feedback/project 类型建议结构：规则/事实 + **Why:** + **How to apply:**}}
```

#### 记忆召回时的验证要求（`TRUSTING_RECALL_SECTION`）

> 一条提到具体函数/文件/flag 的记忆是对"写入时存在"的声明。在推荐给用户前：
> - 路径 → 检查文件存在
> - 函数/flag → grep 确认
> - 如果用户将基于你的推荐行动 → 先验证
>
> "记忆说 X 存在" ≠ "X 现在存在"

#### 保存两步流程
1. 写入独立文件（如 `user_role.md`）
2. 在 `MEMORY.md` 添加单行索引指针：`- [Title](file.md) — one-line hook`

- `MEMORY.md` 截断限制：**200 行** / **25KB**（`MAX_ENTRYPOINT_LINES` / `MAX_ENTRYPOINT_BYTES`）
- 超限时追加警告：`WARNING: MEMORY.md is ... Only part of it was loaded.`

### 自动记忆提取

在每个 query loop 结束时（模型产生无工具调用的最终回复时），后台运行 `extractMemories`：
- 使用 `runForkedAgent`（共享 prompt cache）
- 分析最近消息，提取值得记忆的内容
- 写入自动记忆目录

## 三、Hooks 系统架构

### 3.1 27 种 Hook 事件（完整列表）

源自 `src/entrypoints/sdk/coreSchemas.ts`：

| 事件 | 说明 | Prompt 影响 |
|------|------|------------|
| **PreToolUse** | 工具执行前 | 可阻止调用、修改输入、影响权限（allow/deny/ask） |
| **PostToolUse** | 工具执行后 | 可注入 additionalContext、替换 MCP 工具输出 |
| **PostToolUseFailure** | 工具执行失败后 | 可注入 additionalContext |
| **UserPromptSubmit** | 用户提交 prompt | 可注入 context、可阻止处理 |
| **SessionStart** | 会话开始 | 可注入 context + initialUserMessage + watchPaths |
| **SessionEnd** | 会话结束 | 通知性（紧急超时 1.5s） |
| **Stop** | Claude 结束回复前 | exit code 2 → 注入反馈让 Claude 继续 |
| **StopFailure** | Stop 后 API 错误 | 通知性 |
| **SubagentStart** | 子代理启动 | 可注入 additionalContext |
| **SubagentStop** | 子代理完成 | 类似 Stop |
| **PreCompact** | 压缩前 | stdout 追加为自定义压缩指令 |
| **PostCompact** | 压缩后 | 通知 |
| **PermissionRequest** | [[05 - 权限系统\|权限对话框]] | 可程序化批准/拒绝 + updatedInput |
| **PermissionDenied** | 权限被拒后 | 可设置 retry=true |
| **Setup** | 初始化/维护 | 可注入 context（trigger: 'init'\|'maintenance'） |
| **TeammateIdle** | 团队成员空闲 | exit code 2 → 继续工作 |
| **TaskCreated** | 任务创建 | 可阻止任务创建 |
| **TaskCompleted** | 任务完成 | 可阻止任务完成 |
| **Elicitation** | MCP 询问 | 可自动 accept/decline/cancel |
| **ElicitationResult** | MCP 询问结果 | 可修改/阻止结果 |
| **ConfigChange** | 配置变更 | 审计（策略源不可阻止） |
| **WorktreeCreate** | 工作树创建 | 返回 worktreePath |
| **WorktreeRemove** | 工作树移除 | 通知性 |
| **InstructionsLoaded** | 指令加载时 | 仅审计/观测性 |
| **CwdChanged** | 目录变更 | 可设置环境变量 + watchPaths |
| **FileChanged** | 文件变更 | 可更新环境变量 + watchPaths |
| **Notification** | 通知 | 通知性 |

### 3.2 六种 Hook 类型

1. **command** — Shell 命令（bash 或 PowerShell）
2. **prompt** — 发送 prompt 给 Claude
3. **agent** — 启动 agent 子任务
4. **http** — HTTP POST 请求
5. **callback** — SDK 回调（内部）
6. **function** — 函数 hook（session 范围）

### 3.3 Hook 通信协议

#### Exit Code 语义
- `0` — 成功
- `2` — 阻塞性错误，内容反馈给模型（Stop hook 用此驱动"自动修复"循环）
- 其他非零 — 非阻塞错误，仅显示给用户

#### 同步 JSON 输出 Schema（`syncHookResponseSchema`）

```typescript
// src/types/hooks.ts — Zod schema
z.object({
  continue: z.boolean().optional(),       // false → 阻止 Claude 继续
  suppressOutput: z.boolean().optional(),  // true → 隐藏 stdout
  stopReason: z.string().optional(),       // continue=false 时的终止原因
  decision: z.enum(['approve', 'block']).optional(),
  reason: z.string().optional(),           // decision 的解释
  systemMessage: z.string().optional(),    // 显示给用户的警告
  hookSpecificOutput: z.union([
    // PreToolUse
    z.object({
      hookEventName: z.literal('PreToolUse'),
      permissionDecision: z.enum(['allow', 'deny', 'ask']).optional(),
      permissionDecisionReason: z.string().optional(),
      updatedInput: z.record(z.string(), z.unknown()).optional(),
      additionalContext: z.string().optional(),
    }),
    // UserPromptSubmit
    z.object({
      hookEventName: z.literal('UserPromptSubmit'),
      additionalContext: z.string().optional(),
    }),
    // SessionStart
    z.object({
      hookEventName: z.literal('SessionStart'),
      additionalContext: z.string().optional(),
      initialUserMessage: z.string().optional(),
      watchPaths: z.array(z.string()).optional(),
    }),
    // Setup
    z.object({ hookEventName: z.literal('Setup'), additionalContext: z.string().optional() }),
    // SubagentStart
    z.object({ hookEventName: z.literal('SubagentStart'), additionalContext: z.string().optional() }),
    // PostToolUse
    z.object({
      hookEventName: z.literal('PostToolUse'),
      additionalContext: z.string().optional(),
      updatedMCPToolOutput: z.unknown().optional(),
    }),
    // PostToolUseFailure
    z.object({ hookEventName: z.literal('PostToolUseFailure'), additionalContext: z.string().optional() }),
    // PermissionDenied
    z.object({ hookEventName: z.literal('PermissionDenied'), retry: z.boolean().optional() }),
    // PermissionRequest
    z.object({
      hookEventName: z.literal('PermissionRequest'),
      decision: z.union([
        z.object({ behavior: z.literal('allow'), updatedInput: z.record(...).optional(), updatedPermissions: z.array(...).optional() }),
        z.object({ behavior: z.literal('deny'), message: z.string().optional(), interrupt: z.boolean().optional() }),
      ]),
    }),
    // Elicitation / ElicitationResult
    z.object({ hookEventName: z.literal('Elicitation'), action: z.enum(['accept','decline','cancel']).optional(), content: z.record(...).optional() }),
    z.object({ hookEventName: z.literal('ElicitationResult'), action: z.enum(['accept','decline','cancel']).optional(), content: z.record(...).optional() }),
    // CwdChanged / FileChanged
    z.object({ hookEventName: z.literal('CwdChanged'), watchPaths: z.array(z.string()).optional() }),
    z.object({ hookEventName: z.literal('FileChanged'), watchPaths: z.array(z.string()).optional() }),
    // WorktreeCreate
    z.object({ hookEventName: z.literal('WorktreeCreate'), worktreePath: z.string() }),
  ]).optional(),
})
```

#### 异步 Hook 协议（`asyncHookResponseSchema`）

Hook 可声明自己是异步的，方式有二：

1. **配置声明**：在 settings.json 中设置 `async: true` 或 `asyncRewake: true`
2. **运行时声明**：在 stdout 第一行输出 `{"async": true}`

```typescript
z.object({
  async: z.literal(true),
  asyncTimeout: z.number().optional(),  // 异步执行超时（ms）
})
```

- 异步 hook 被注册到 `AsyncHookRegistry` 中，进程在后台运行
- `asyncRewake` 模式：完成后若 exit code 2，通过 `enqueuePendingNotification` 唤醒模型
- 最终 schema 是 `z.union([asyncHookResponseSchema, syncHookResponseSchema])`

#### 权限决策优先级

多个 hook 同时返回权限决策时：**deny > ask > allow**

### 3.4 Stop Hook — CI/CD 集成关键

> [!tip] 自动修复循环的关键机制
> Stop Hook 是实现"运行测试 → 失败则自动修复"循环的核心。exit code 2 的特殊语义使得外部程序可以将反馈注入模型上下文。

```
Claude 完成回复 → 运行 Stop hook（如执行测试）
                  → exit code 0：正常结束
                  → exit code 2：stderr 注入为反馈，迫使 Claude 继续修复
```

这是实现"运行测试 → 失败则自动修复"循环的关键机制。

`asyncRewake` 模式的 Stop hook 更进一步：
- hook 在后台运行，不阻塞模型响应
- 完成后若 exit code 2，通过 `enqueuePendingNotification` 将错误注入为 `task-notification`
- 模型空闲时被唤醒处理，忙碌时注入为 `queued_command` 附件

### 3.4.1 Hook Matcher 与 `if` 条件

Hook 匹配支持三种 matcher 模式：
- **精确匹配**：`"Write"`
- **管道分隔多匹配**：`"Write|Edit"`
- **正则表达式**：`"^Write.*"`, `".*"`, `"^(Write|Edit)$"`

`if` 条件允许更精细的过滤（如只对特定 Bash 命令触发）：
```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "if": "Bash(git *)",
      "hooks": [{ "type": "command", "command": "echo 'git command detected'" }]
    }]
  }
}
```

### 3.5 系统 Prompt 中的 Hooks 说明

在 [[14 - System Prompt 架构]] 中注入：

```
Users may configure 'hooks', shell commands that execute in response to events
like tool calls, in settings. Treat feedback from hooks, including
<user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook,
determine if you can adjust your actions in response to the blocked message.
If not, ask the user to check their hooks configuration.
```

### 3.6 安全机制

- **工作区信任检查**：交互模式需用户接受信任对话框
- **企业控制**：`disableAllHooks` 禁用所有 hooks
- **受管理模式**：`shouldAllowManagedHooksOnly` 仅运行管理员 hooks

## 四、配置系统对 Prompt 的影响

> 另见 [[11 - 配置与内存系统]] 获取完整配置架构。

### 4.1 四层配置来源

| 来源 | 路径 | 优先级 |
|------|------|-------|
| **policySettings** | 管理员路径 | 最高 |
| **userSettings** | `~/.claude/settings.json` | 高 |
| **projectSettings** | `.claude/settings.json` | 中 |
| **localSettings** | `.claude/settings.local.json` | 低 |

### 4.2 影响 Prompt 的关键配置

| 字段 | 影响 |
|------|------|
| `hooks` | 定义 27 种事件的 hook 命令 |
| `permissions` | allow/deny/ask 规则 |
| `language` | 注入语言偏好到 system prompt |
| `claudeMdExcludes` | 排除特定 CLAUDE.md 文件 |
| `settingSources` | 控制哪些配置来源被启用 |
| `outputStyle` | 注入输出风格到 system prompt |

### 4.3 语言注入

```
# Language
Always respond in {language}. Use {language} for all explanations, comments, 
and communications. Technical terms and code identifiers remain in original form.
```

## 五、/remember 命令

仅 ant 内部用户可用（`process.env.USER_TYPE !== 'ant'` 时不注册），依赖 `isAutoMemoryEnabled()`。

### 完整 Skill Prompt

来自 `src/skills/bundled/remember.ts`：

```markdown
# Memory Review

## Goal
Review the user's memory landscape and produce a clear report of proposed changes,
grouped by action type. Do NOT apply changes — present proposals for user approval.

## Steps

### 1. Gather all memory layers
Read CLAUDE.md and CLAUDE.local.md from the project root (if they exist).
Your auto-memory content is already in your system prompt — review it there.
Note which team memory sections exist, if any.

### 2. Classify each auto-memory entry
For each substantive entry in auto-memory, determine the best destination:

| Destination | What belongs there | Examples |
|---|---|---|
| **CLAUDE.md** | 项目约定，所有贡献者应遵循 | "use bun not npm", "test command is bun test" |
| **CLAUDE.local.md** | 个人偏好，不适用于其他贡献者 | "I prefer concise responses", "don't auto-commit" |
| **Team memory** | 跨仓库的组织知识（需已配置） | "deploy PRs go through #deploy-queue" |
| **Stay in auto-memory** | 临时上下文、不确定归属 | Session-specific observations |

**Important distinctions:**
- CLAUDE.md/CLAUDE.local.md 是给 Claude 的指令，不是外部工具偏好
- 工作流实践（PR 约定等）属于模糊地带 — 应询问用户是个人还是团队
- 不确定时，询问而非猜测

### 3. Identify cleanup opportunities
- **Duplicates**: auto-memory 已在 CLAUDE.md 中 → 移除
- **Outdated**: 被更新的 auto-memory 矛盾 → 更新
- **Conflicts**: 任意两层矛盾 → 提议解决

### 4. Present the report
1. **Promotions** — 移动建议（含目标和理由）
2. **Cleanup** — 重复、过时、冲突
3. **Ambiguous** — 需用户决定
4. **No action needed** — 保持原状

## Rules
- Present ALL proposals before making any changes
- Do NOT modify files without explicit user approval
- Do NOT create new files unless the target doesn't exist yet
- Ask about ambiguous entries — don't guess
```

## 六、Session Memory（会话记忆）

独立于 auto-memory 的结构化会话笔记：
- 模板在 `~/.claude/session-memory/config/template.md`
- 固定结构：Session Title、Current State、Task Specification、Files and Functions 等
- 每次 compact 时通过 forked agent 更新
- 最大 12000 tokens

## 七、Compact 时的 Memory 重新加载

> 另见 [[16 - Prompt Caching 与 Context 管理]] 中关于压缩策略的详细说明。

```
compact → resetGetMemoryFilesCache('compact')
       → 清除 memoize 缓存
       → shouldFireHook = true, nextEagerLoadReason = 'compact'
       → 下次 getMemoryFiles() 重新加载所有 memory 文件
       → InstructionsLoaded hooks 以 load_reason='compact' 触发
```

> [!note] clearMemoryFileCaches vs resetGetMemoryFilesCache
> - `clearMemoryFileCaches()`：仅清缓存，**不触发** InstructionsLoaded hook（用于 worktree 进出、settings 同步）
> - `resetGetMemoryFilesCache(reason)`：清缓存 + 设置 hook 触发标志（用于 compact 等真正重新加载指令的场景）

## 八、实践启示

1. **Memory 放 User Message 而非 System Prompt**：CLAUDE.md 内容通过 `<system-reminder>` 注入到第一条 user message 中，而非 system prompt。这保持了 system prompt 的缓存稳定性——CLAUDE.md 经常变化，放在 system prompt 中会频繁 bust cache。这一设计决策在任何需要注入动态配置的 LLM 应用中都值得借鉴。

2. **六层 Memory 的优先级覆盖模型**：从 Managed（全局最低）到 TeamMem（最高），后加载覆盖先加载。这种"层叠配置"模式与 CSS 优先级类似——企业策略 → 用户偏好 → 项目约定 → 本地覆盖，每层有明确的职责边界。

3. **Hook Exit Code 语义的精妙**：exit code 2 的"阻塞性反馈"机制是实现 CI/CD 自动修复循环的关键。这比简单的 success/failure 二值语义更有表现力——`0` 是放行、非零非 2 是警告、`2` 是"把错误反馈给模型让它继续"。这种三值语义可推广到任何需要外部工具与 LLM 交互的场景。

4. **记忆排除规则的反直觉设计**：Auto-memory 明确排除"可从当前项目状态推导的内容"（代码模式、架构、文件路径），即使用户要求保存也不保存。这是对 LLM 记忆系统的深刻认知——代码会变，记住的路径/模式很快过期，反而会误导。只记"非显而易见的 why"而非"可查到的 what"。

5. **多 Hook 权限决策的合并策略**：`deny > ask > allow` 的优先级确保安全性——多个 hook 中只要一个说 deny，最终就是 deny。这是"默认安全"原则在事件系统中的体现。
